﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.IO;
using System.Net.Sockets;
using System.ComponentModel;
using System.Windows.Forms;
using System.Threading;
using System.Collections;
using System.Net;
using System.Timers;

namespace ChatClient
{
    class MessageClient
    {
        private TcpClient tcpClient;
        private int connectAttempts = 0;
        private int maxConnectAttempts = 2;
        private bool running;
        private Queue<string> outMessages;

        private readonly ConnectionWorker connectionWorker;
        private StreamReader reader;
        private StreamWriter writer;

        private String serverAddress;
        private IPAddress ipAddress;
        private readonly int portNumber = 49222;
        private char SPLIT_CHAR = (char)23;
        private bool serverShutdown = false;
        private String[] userList;
        private String[] chatroomList;

        private System.Timers.Timer lockoutTimer;
        private bool lockoutEnabled;
        private int lockoutInterval = 5000;

        public delegate void GroupDelegate(string chatroomID, string message);
        public event GroupDelegate updateGroupChat;

        public delegate void LoginDelegate();
        public event LoginDelegate confirmLogin;
        public event LoginDelegate rejectLogin;

        public delegate void LogoutDelegate(String shutdownMessage);
        public event LogoutDelegate showShutdownPopup;

        public delegate void UpdateListDelegate();
        public event UpdateListDelegate promptChatroomListUpdate;

        public delegate void FloodLockoutDelegate();
        public event FloodLockoutDelegate floodLockoutStarted;
        public event FloodLockoutDelegate floodLockoutWarning;



        public MessageClient()
        {
            connectionWorker = new ConnectionWorker(this);
            outMessages = new Queue<string>();
            initialiseLockoutTimer();

            // Set default network address for chat server.
            serverAddress = "127.0.0.1";
            ipAddress = IPAddress.Parse(serverAddress);
        }

        /** 
         *  Return the client/server formatting character. 
         */
        public char getSplitChar()
        {
            return SPLIT_CHAR;
        }

        public void initialiseLockoutTimer()
        {
            lockoutTimer = new System.Timers.Timer();
            lockoutTimer.Interval = lockoutInterval;
            lockoutTimer.Elapsed += new ElapsedEventHandler(resetFloodTimer);

            lockoutEnabled = false;
        }

        public void setServerAddress(String IPAddressString)
        {
            ipAddress = IPAddress.Parse(IPAddressString);

        }

        public void Start()
        {
            if (running) return;
            System.Diagnostics.Debug.WriteLine("Attempting connection...");
            running = true;
            bool connected = false;

            outMessages.Clear();

            // Attempt a connection to the server. Retry if the attempt fails.
            try
            {
                tcpClient = new TcpClient(serverAddress, portNumber);
            }

            // First attempt failed, retry connection.
            catch (SocketException)
            {
                System.Diagnostics.Debug.WriteLine("Connection failed. Retrying...");
                connectAttempts = 0;

                // Keep retrying connection until the attempt limit is exceeded.
                while (!connected)
                {
                    try
                    {
                        connectAttempts++;
                        tcpClient = new TcpClient(serverAddress, portNumber);
                        connected = true;
                    }
                    catch (SocketException)
                    {
                        if (connectAttempts >= maxConnectAttempts)
                        {
                            System.Diagnostics.Debug.WriteLine("Retry limit exceeded.");
                            running = false;
                            return;
                        }
                    }
                }
            }

            reader = new StreamReader(tcpClient.GetStream());
            writer = new StreamWriter(tcpClient.GetStream());

            connectionWorker.RunWorkerAsync();
        }

        /** Stop
         * Notify the server before stopping send and received loop and closing the client's socket connection.
         */
        public void Stop()
        {
            if (!running)
            { return; }

            // Only send a logout request if program is shut down client-side.
            if (!serverShutdown)
            { sendThrottledMessage("LOGOUT"); }

            tcpClient.Close();
            connectionWorker.CancelAsync();
            running = false;
            serverShutdown = false;

        }

        /** rot46(String)
         * Use the rot46 encryption algorithm to encrypt/decrypt.
         * As rot46 is a reciprocal cipher, any encrypted characters can be decrypted by passing them through again.
         * 
         * @param str String to be encrypted/decrypted 
         * @return Encrypted or decrypted string
         */
        public String rot46(String str)
        {
            String encryptedString = "";

            // Perform encryption one character at a time.
            foreach (char c in str.ToCharArray())
            {
                encryptedString += rot46(c);
            }
            return encryptedString;
        }

        /** rot46(char) 
         * Use the rot46 encryption algorithm to encrypt/decrypt a single character.
         * As rot46 is a reciprocal cipher, any encrypted characters can be decrypted by passing them through again.
         * 
         * @param c Character to be encrypted/decrypted
         * @return Encrypted or decrypted character
         */
        public char rot46(char c)
        {
            const int encryptionOffset = 46;
            if (c != SPLIT_CHAR)
            {
                if (c >= '!' && c <= 'P') { c += (char)encryptionOffset; }
                else if (c >= 'Q' && c <= '~') { c -= (char)encryptionOffset; }
            }
            return c;
        }

        /** sendThrottledMessage
         * Send a message to the server using an anti-flooding protocol.
         * If a client sends too many messages in a short amount of time, the user will be locked out and the messages will not send.
         */
        public void sendThrottledMessage(string message)
        {
            // Don't transmit message if the client is in chat flood lockout.
            if (!lockoutEnabled)
            {
                // Deliver message and start the timer for the next message.
                sendMessage(message);
            }
            else
            {
                //TODO floodLockoutWarning();
                System.Diagnostics.Debug.WriteLine("Locked out");
            }

        }

        /** sendMessage
         * Send an unthrottled, encrypted message to the server.
         */
        private void sendMessage(string message)
        {
            String encryptedMessage = rot46(message);
            lock (outMessages)
            { outMessages.Enqueue(encryptedMessage); }
        }

        /** startLockout
         * Begin the client-side anti-flooding lockout and notify the server.
         * This prevents the user from sending any chat messages to the server.
         */
        private void startLockout()
        {
            lockoutEnabled = true;
            lockoutTimer.Interval = lockoutInterval;
            lockoutTimer.Start();
            System.Diagnostics.Debug.WriteLine("Lockout started");
            //TODO floodLockoutStarted();
        }

        /** 
         * Return whether or not the client is actively connected to the server.
         */
        public bool isRunning()
        {
            return running;
        }

        /**
         * Return an array of the current group chats.
         */
        public String[] getChatroomList()
        {
            return chatroomList;
        }

        /** 
         * Send a request for the server's active user list.
         */
        public void requestUserList()
        {
            sendMessage("GET_USERS");
        }

        /** sanitiseMessage
         * Remove any instances of the newline, and split characters from a string.
         */
        public String sanitiseMessage(String message)
        {
            // Remove separator character.
            message = message.Replace(SPLIT_CHAR.ToString(), "");

            // Remove newline chars.
            message = message.Replace("\n", " ");
            return message;
        }

        /** sanitiseUsername
         * Remove any instance of the newline, comma, and split characters from string.
         * This is typically only used for usernames, as messages should allow commas.
         */
        public String sanitiseUsername(String username)
        {
            // Remove newline and split characters
            username = sanitiseMessage(username);

            // Remove commas
            username = username.Replace(",", "");

            return username;
        }

        /** formatGroupMessage
         * 
         * Take a string message, remove any system-dependant characters, and set the private message,
         * and destination tags.
         */
        public String formatGroupMessage(String destination, String message)
        {
            message = sanitiseMessage(message);
            String formattedString = "GROUP" + SPLIT_CHAR + destination + SPLIT_CHAR + message;
            return formattedString;
        }

        /** resetFloodTimer
         * Elapsed timer event for the chat throttling timer.
         * When this event is reached, an adequete time has passed between messages, so any flooding stats are reset.
         */
        private void resetFloodTimer(object source, ElapsedEventArgs e)
        {
            // Stop and reinitialise the default timer values
            lockoutTimer.Stop();
            lockoutEnabled = false;
        }

        /**
         * Send a request to the server for an updated chatroom list.
         * This message is not throttled and can be sent while the user is locked out.
         */
        public void requestChatroomList()
        {
            sendMessage("GET_CHATROOMS");
        }

        class ConnectionWorker : BackgroundWorker
        {
            private MessageClient _outer;

            public ConnectionWorker(MessageClient outer)
            {
                _outer = outer;
                WorkerReportsProgress = false;
                WorkerSupportsCancellation = true;
            }

            protected override void OnDoWork(DoWorkEventArgs e)
            {
                _outer.running = true;

                // Listening Thread Loop
                Thread listenerThread = new Thread(() =>
                {
                    try
                    {
                        while (_outer.running && !CancellationPending)
                        {
                            // Wait for message. Blocking method.
                            String incomingMsg = _outer.reader.ReadLine();
                            handleIncoming(incomingMsg);
                        }
                    }
                    catch (IOException error)
                    {
                        System.Diagnostics.Debug.WriteLine("Error: " + error);
                    }
                });
                listenerThread.IsBackground = true;
                listenerThread.Start();

                try
                {
                    // Sending Loop
                    while (_outer.running && !CancellationPending)
                    {
                        Application.DoEvents();
                        lock (_outer.outMessages)
                        {
                            if (_outer.outMessages.Count != 0)
                            {
                                handleOutgoing(_outer.outMessages);
                            }
                            else
                            {
                                Thread.Sleep(200);
                            }
                        }
                    }
                }
                catch (Exception error)
                {
                    System.Diagnostics.Debug.WriteLine("error: " + error);
                }
                finally
                {
                    listenerThread.Interrupt();
                    _outer.Stop();

                }
            }

            /** 
             * Process the incoming string using the established data protocol.
             */
            private void handleIncoming(String incomingRequest)
            {
                String decryptedMessage = _outer.rot46(incomingRequest);
                System.Diagnostics.Debug.WriteLine("IN: " + decryptedMessage);

                String[] requestArgs = decryptedMessage.Split(_outer.SPLIT_CHAR);
                String messageTag = requestArgs[0];

                try
                {
                    // STOP, <shutdown message> - Closes the connection to the server from the client side.
                    if (requestArgs[0].StartsWith("STOP"))
                    { shutDownClient(requestArgs[1]); }

                    // LOCKOUT - Stop the client from sending chat messages for a short time
                    else if (requestArgs[0].StartsWith("LOCKOUT"))
                        _outer.startLockout();

                    // GROUP, <chatroomID>, <message> - Display incoming group message on the main chat window
                    else if (messageTag.StartsWith("GROUP"))
                    { _outer.updateGroupChat(requestArgs[1], requestArgs[2]); }

                    // LOGIN, <server response> - Deal with the response from a login attempt.
                    else if (messageTag.StartsWith("LOGIN"))
                    { handleLoginResponse(requestArgs[1]); }

                    // USERS, <user1,user2,user3,...> - Update the client's registered user list.
                    else if (messageTag.StartsWith("USERS"))
                    {
                        String[] newUserList = requestArgs[1].Split(',');
                        _outer.userList = newUserList;
                    }

                    // CHATROOMS, <chatroom1,chatroom2,...> - Update the client's chatroom list.
                    else if (messageTag.StartsWith("CHATROOMS"))
                    {
                        handleNewChatroomList(requestArgs[1]);
                    }
                }
                catch (IndexOutOfRangeException e)
                { System.Diagnostics.Debug.WriteLine("Input length mismatch: " + e); }
            }

            /**
             * Extract the new chatroom list and push the changes to the GUI.
             */
            private void handleNewChatroomList(String chatroomString)
            {
                chatroomString = chatroomString.Replace(" ", "");
                _outer.chatroomList = chatroomString.Split(',');
                _outer.promptChatroomListUpdate();
            }

            /** 
             * Shut down the client after receiving a stop message from the server.
             * A popup event is available to show a shutdown message.
             */
            private void shutDownClient(String shutdownMessage)
            {
                _outer.serverShutdown = true;
                _outer.showShutdownPopup(shutdownMessage);
                _outer.Stop();
            }

            /** handleLoginResponse
             * Raise an event depending on the response of a login request.
             */
            private void handleLoginResponse(String response)
            {
                if (response.Equals("OK"))
                { _outer.confirmLogin(); }
                else
                { _outer.rejectLogin(); }
            }

            /** handleOutgoing
             * Send any messages that are present in the outgoing queue.
             */
            private void handleOutgoing(Queue<string> queue)
            {
                var message = queue.Dequeue();
                _outer.writer.WriteLine(message);
                _outer.writer.Flush();

                System.Diagnostics.Debug.WriteLine("Out: " + message);
            }
        }

        internal void requestLogin(string username)
        { sendMessage("LOGIN" + SPLIT_CHAR + username); }
    }
}


